# This file is part of Gehyra.
#
# Gehyra is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Gehyra is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gehyra.  If not, see <http://www.gnu.org/licenses/>.

"""@package gehyra.bootstrap.manager
Manager for all bootstrap collection modules.
Object which deals with collecting the bootstrap information from 
locations declared in the network configuration.
$Id: manager.py 450 2011-01-20 14:28:47Z andyhhp@gmail.com $
"""

"""@file gehyra/bootstrap/manager.py
Manager for all bootstrap collection modules.
Object which deals with collecting the bootstrap information from 
locations declared in the network configuration.
"""

import sys
import time

from gehyra.common.logger import LOG
from gehyra.common.interfaces import (IBootstrapManager,
                                      IBootstrapPuller,
                                      IGlobalObserver)

from zope.interface import implements
from zope.interface.verify import verifyObject

from twisted.internet import reactor


class Manager(object):
    """Bootstrap Manager.
    Collects the bootstrap information from the configured sources, as
    specified in the network configuration
    """
    implements(IBootstrapManager, IGlobalObserver)

    
    def __init__(self, main):
        """Constructor.
        @param main Main gehyra obhect
        """
    
        ## Main gehyra object
        self.main = main

        ## List of IBootstrap objects
        self.boot_list = []

        ## Latest valid bootstrap lookup
        self.latest = {}

        ## Error from latest lookup attempt
        self.error = "Not run yet"

        ## Generator for bootstrap methods
        self.method_gen = None

    
    def initialize(self):
        """Initalise things.
        Read the bootstrap configuration from network.cfg and create a list of
        bootstrap puller objects.
        """
        cfg = self.main.config_manager
        
        for name in cfg.network['bootstrap']['priority']:

            try:
                t = cfg.network['bootstrap'][name+".type"]
                a = cfg.network['bootstrap'][name+".args"]
            except KeyError:
                LOG.warn("Bootstrap object '%s' must have a type and arguments"
                         % name)
                continue

            LOG.debug("Found bootstrap option: %s of type %s" % (name, t) )

            # This is where the magic happens:
            # dynamically import the specified module
            mod_name = 'gehyra.bootstrap.modules.pull_'+t
            try:
                __import__(mod_name, globals(), locals(), [], -1)
            except ImportError:
                LOG.warn("Unable to import '%s' as a bootstrap puller"
                         % mod_name)
                continue

            # create an instance of that object with the specified parameters
            mod_object = sys.modules[mod_name].__dict__['pull_'+t](name, **a)
            verifyObject(IBootstrapPuller, mod_object)

            # append to bootstrap list
            if mod_object.initialize():
                self.boot_list.append(mod_object)

        if len(self.boot_list) < 1:
            LOG.warn("No valid bootstrap methods found")

        self.main.hook_observer(self)
        return True

    def _queue_next(self):
        """run through all bootstrap methods"""
        try:
            n = self.method_gen.next()
        except StopIteration:
            return

        d = n.collect()

        def success(results):
            """defered success"""
            if "error" in results:
                self.error = results['error']
                reactor.callLater(0, self._queue_next)
                return

            if 'push_timestamp' not in results:
                self.error = "Malformed results"
                reactor.callLater(0, self._queue_next)
                return
            else:
                try:
                    push_ts = int(results['push_timestamp'])
                except ValueError:
                    push_ts = 1
                
            try:
                cur_ts =  int(self.latest['push_timestamp'])
            except (KeyError, ValueError):
                cur_ts = 1

            if cur_ts < push_ts:
                self.latest = results
                self.main.bootstrap_updated()
            else:
                self.error = "Newer configuration already cached"


            if push_ts < (time.time() - 15*60):
                reactor.callLater(0, self._queue_next)

        def error(error):
            """defered error"""
            LOG.error(error)
            self.error = "Error occured"

            reactor.callLater(0, self._queue_next)

        def success_fn_error(error):
            """This really really shoulnt happen"""
            LOG.critical(error)
            LOG.critical("This really really shouldnt happen")

        d.addCallbacks(success, error)
        d.addErrback(success_fn_error)


    def pull(self):
        """Pull the latest bootstrap information"""
        
        if len(self.boot_list) < 1:
            self.error = "No valid entries in list"
            return

        self.method_gen = ( x for x in self.boot_list )
        
        LOG.info("Starting dynamic config lookup")
        reactor.callLater(0, self._queue_next)


    # pylint: disable=C0301
    # ## @BRIEu Start the initial search for dconfig data
    # #
    # # Start the search for bootstrap information.  It works through the list of dconfig options
    # # provided in network.cfg and schedules them to run.  It will attempt the methods in the order
    # # stated in the config.  It will return data using the callback function for the first success.
    # # The timeout applies only to waiting requests after the alotted time.  If all methods have
    # # been tried and failed, the callback is called with the an error message
    # # @param self Pointer to self
    # # @param callback Callback function to return information
    # # @param timeout Optional number of seconds before failing (0 means no timeout)
    # # @return Nothing.  A dictionary containing the following is returned using the callback function
    # # - 'error': String containing error information, if any.
    # # - 'website': Website URL, including protocol.
    # # - 'upgrade': URL containing the upgrade directory.  Either a complete URL or a 
    # #   fragment to be appended to the website URL.
    # # - 'min_ver': Minimum version string
    # # - 'latest_ver': Latest version avaliable
    # # - 'min_share': Minimum share in bytes
    # # - 'pk_hash': Hash of the bridge public key
    # # - 'ipcache': List of nodes to contact
    # def initialCollection(self, callback, timeout = 30):
        
    #     ret_default = { 'error' : None,
    #                     'website' : None,
    #                     'upgrade' : None,
    #                     'min_ver' : None,
    #                     'latest_ver' : None,
    #                     'min_share' : None,
    #                     'pk_hash' : None,
    #                     'ipcache' : None }
        
    #     # If we have no bootstrap methods to start with
    #     # return a defer.fail object which signals an error
    #     if len(self.boot_list) < 1:
        
    #         def none_to_start_callback(res):
    #             callback({'Error': 'No bootstrap methods'})
    #             return None
            
    #         return defer.fail(failure.DefaultException()).addErrback(none_to_start_callback)
        

                        
    #     LOG.info("Starting initial dconfig collection")
        
    #     # basic timeout error 
    #     def timeout_callback():

    #         cancel_dcall(self.start_next_deferred)
    #         while len(active) > 0:
    #             o = active.pop()
    #             cancel_dcall(o)

    #         callback({'Error': 'Timeout'})
        
    #     # get the reactor to interveen later if we hit the timeout (0 means no timeout)
    #     if timeout != 0:
    #         self.init_timeout_deferred = reactor.callLater(timeout,timeout_callback)
        
    #     trys = deque(self.boot_list)
    #     active = []
        
    #     # callback function to start the next method is the previous one is being slow
    #     def start_next():

    #         next = trys.popleft()
    #         defer = next.collect(ret_default)
    #         active.append(defer)
    #         LOG.debug("Dconfig - starting '%s'" % next.text())
            
    #         # hack to bypass the python scoping system
    #         def gen_cb(d):
            
    #             # success callback:
    #             # cancel all other active deferred
    #             # parse data and return in dictionary form
    #             def success_callback(result):
    #                 cancel_dcall(self.start_next_deferred)
    #                 cancel_dcall(self.init_timeout_deferred)
                    
    #                 while len(active) > 0:
    #                     o = active.pop()
    #                     if id(o) != id(d):
    #                         cancel_dcall(o)
                    
                    
    #                 callback(result)
                
    #             # error callback
    #             # if no more methods are active and no more trys are left
    #             # give up now instead of waiting for the timeout
    #             def error_callback(error):
    #                 # remove myself from the active list
    #                 active.remove(d)
                    
    #                 # if there are no active requests
    #                 if len(active) == 0:
    #                     # if there are also no trys left, we have failed at
    #                     # all methods so give up
    #                     if len(trys) == 0:
    #                         cancel_dcall(self.init_timeout_deferred)
    #                         callback({'Error': 'All methods failed'})
                        
    #                     # if there are no active requests and we are waiting
    #                     # for another request to be scheduled, start scheduling
    #                     # it now
    #                     if self.start_next_deferred.active():
    #                         self.start_next_deferred.reset(0)
                
    #             return {'callback':success_callback, 'errback':error_callback}
            
    #         defer.addCallbacks(**gen_cb(defer))
            
    #         if len(trys) != 0:
    #             self.start_next_deferred = reactor.callLater(3,start_next)
                

    #     self.start_next_deferred = reactor.callLater(0,start_next)
    # pylint: enable=C0301

    def on_event(self, event):
        """Observe event.
        If we hear a startsync event, attempt to pull the config
        """
        if event == self.main.EVENT_STARTSYNC:
            reactor.callLater(0, self.pull)

    def on_state(self, event):
        """Observe state
        Nothing to do
        """
        pass

